#region MIT License

/*
 *  Copyright (c) 2010 Nathan Palmer
 *  http://www.nathanpalmer.com/
 * 
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 * 
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 * 
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 */

#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Common.Logging;
using Quartz;
using Quartz.Spi;
using SimpleScheduler.Common;
using SimpleScheduler.Core.Common;
using SimpleScheduler.Core.Domain;
using Trigger = Quartz.Trigger;

namespace SimpleScheduler.Server.Plugin
{
    public class XmlJobPlugin : ISchedulerPlugin, IJobListener, ISchedulerListener
    {
        private IScheduler scheduler;
        private ILog log;
        private Dictionary<DirectoryInfo, FileWatcher> directoryWatches;
        private Dictionary<string, Core.Domain.Job> jobDictionary;
        private List<string> jobsToRemove;
        
        private List<FileInfo> jobsAdded;
        private List<FileInfo> jobsSaved;

        private string folderPath;
        public virtual string FolderPath
        {
            get { return folderPath; }
            set { folderPath = ResolveFile(value); }
        }

        protected internal static string ResolveFile(string fName)
        {
            if (fName != null && fName.StartsWith("~"))
            {
                // relative to run directory
                fName = fName.Substring(1);
                if (fName.StartsWith("/") || fName.StartsWith("\\"))
                {
                    fName = fName.Substring(1);
                }
                fName = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, fName);
            }

            return fName;
        }

        public XmlJobPlugin()
        {
            log = LogManager.GetLogger(GetType());
            jobsAdded = new List<FileInfo>();
            jobsSaved = new List<FileInfo>();
        }

        public void Initialize(string pluginName, IScheduler sched)
        {
            scheduler = sched;
            scheduler.AddGlobalJobListener(this);
            scheduler.AddSchedulerListener(this);
            log.Info("Registering Xml Job Plug-in");
        }

        public void Start()
        {
            try
            {
                // Create job dictionary
                jobsToRemove = new List<string>();
                jobDictionary = new Dictionary<string, Core.Domain.Job>();

                // Load all jobs currently in directory
                LoadJobs();

                // Start file watcher
                directoryWatches = new Dictionary<DirectoryInfo, FileWatcher>();
                foreach (var directory in new DirectoryInfo(FolderPath.Replace("/", "\\")).GetDirectories())
                {
                    var fileWatcher = new FileWatcher(directory.FullName);
                    fileWatcher.Changed += new FileSystemEventHandler(FileWatcher_Changed);
                    fileWatcher.Deleted += new FileSystemEventHandler(FileWatcher_Deleted);
                    fileWatcher.Renamed += new RenamedEventHandler(FileWatcher_Renamed);
                    directoryWatches.Add(directory, fileWatcher);
                }
            }
            catch (Exception ex)
            {
                log.Fatal(string.Format("Could not start the XmlJobPlugin.\r\n\r\n{0}", ex.ToString()));
            }
        }

        void FileWatcher_Renamed(object sender, RenamedEventArgs e)
        {
            // Remove the job scheduled under the old name
            FileInfo oldFileInfo = new FileInfo(e.OldFullPath);
            if (jobDictionary.ContainsKey(oldFileInfo.FullName))
            {
                RemoveJob(oldFileInfo);
            }

            LoadJob(new FileInfo(e.FullPath));            
        }

        void FileWatcher_Deleted(object sender, FileSystemEventArgs e)
        {
            RemoveJob(new FileInfo(e.FullPath));
        }

        void FileWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            LoadJob(new FileInfo(e.FullPath));
        }

        private void LoadJobs()
        {
            var directoryInfo = new DirectoryInfo(FolderPath);
            foreach(var directory in directoryInfo.GetDirectories())
            {
                foreach (var file in directory.GetFiles("*.xml"))
                {
                    try
                    {
                        LoadJob(file);
                    }
                    catch (Exception ex)
                    {
                        log.Error(string.Format("Attempted to load job from file {0} but received an error.\r\n\r\n{1}", file.FullName, ex.ToString()));
                    }
                }
            }
        }

        private void RemoveJob(FileInfo fileInfo)
        {
            var job = jobDictionary[fileInfo.FullName];
            RemoveJob(fileInfo.Directory.Name, job);
            jobDictionary.Remove(fileInfo.FullName);
            log.Info(string.Format("Removed job {0} for file {1}", job.Name, fileInfo.Name));
        }

        private void RemoveJob(string Group, Core.Domain.Job Job)
        {
            if (scheduler.GetCurrentlyExecutingJobs().Cast<JobExecutionContext>().Any(j => j.JobDetail.Name == Job.Name && j.JobDetail.Group == Group))
            {
                var jobFullName = string.Format("{0}.{1}", Group, Job.Name);
                lock (jobsToRemove)
                {
                    jobsToRemove.Add(jobFullName);
                }

                while (jobsToRemove.Contains(jobFullName))
                {
                    log.Warn(string.Format("Pending removal. Job {0}.{1} currently executing.", Group, Job.Name));
                    Thread.Sleep(5000);
                }
            }
            else
            {
                if (!scheduler.DeleteJob(Job.Name, Group))
                {
                    log.Error(string.Format("Unable to remove job {0}", Job.Name));
                    Thread.Sleep(5000);
                    WaitUntilComplete("removal", Job.Name, Group);
                    
                    //TODO: Infinite loop here.. should probably remove
                    RemoveJob(Group, Job);
                }
            }
        }

        private void WaitUntilComplete(string Message, string JobName, string GroupName)
        {
            while (scheduler.GetCurrentlyExecutingJobs().Cast<JobExecutionContext>().Any(j => j.JobDetail.Name == JobName && j.JobDetail.Group == GroupName))
            {
                log.Warn(string.Format("Pending {0}. Job {1}.{2} currently executing.", Message, GroupName, JobName));
                Thread.Sleep(1000);
            }
        }

        private void LoadJob(FileInfo file)
        {
            if (!file.Extension.Equals(".xml", StringComparison.InvariantCultureIgnoreCase))
            {
                return;
            }

            var fileInfo = jobsSaved.Where(f => f.FullName.Equals(file.FullName, StringComparison.CurrentCultureIgnoreCase)).FirstOrDefault();
            if (fileInfo != null)
            {
                jobsSaved.Remove(fileInfo);
                return;
            }

            var jobName = file.Name.Replace(file.Extension, "");
            var groupName = file.Directory.Name;
            var xml = "";

            using (var fs = file.OpenText())
            {
                xml = fs.ReadToEnd();
                fs.Close();
            }

            var job = xml.XmlDeserialize<Core.Domain.Job>();
            job.Name = jobName;

            JobDetail quartzJob;
            if (jobDictionary.ContainsKey(file.FullName))
            {
                var tempJob = jobDictionary[file.FullName];
                quartzJob = scheduler.GetJobDetail(tempJob.Name, file.Directory.Name);
            }
            else
            {
                quartzJob = new JobDetail(jobName, groupName, Type.GetType(job.Type));
                jobDictionary.Add(file.FullName, job);
            }

            quartzJob.Description = job.Description;
            quartzJob.JobDataMap["Tasks"] = job.Tasks;
            quartzJob.JobDataMap["Triggers"] = job.Triggers;
            quartzJob.JobDataMap["Type"] = job.Type;
            quartzJob.JobDataMap["Enabled"] = job.Enabled;
            jobsAdded.Add(file);
            scheduler.AddJob(quartzJob, true);
            

            log.Info(string.Format("Job {0}.{1} loaded", groupName, job.Name));

            if (job.Enabled)
            {
                Trigger[] quartzTriggers = scheduler.GetTriggersOfJob(quartzJob.Name, quartzJob.Group).ToArray();

                foreach (var trigger in job.Triggers)
                {
                    Trigger newTrigger = null;
                    switch (trigger.Type)
                    {
                        case "Simple":
                            SimpleTrigger simpleTrigger = new SimpleTrigger(Guid.NewGuid().ToString(), groupName);

                            if (trigger.StartTime.HasValue)
                                simpleTrigger.StartTimeUtc = trigger.StartTime.Value.ToUniversalTime();
                            if (trigger.EndTime.HasValue)
                                simpleTrigger.EndTimeUtc = trigger.EndTime.Value.ToUniversalTime();

                            simpleTrigger.RepeatCount = (trigger.RepeatCount == null) ? SimpleTrigger.RepeatIndefinitely : trigger.RepeatCount.Value;
                            if (trigger.RepeatInterval.HasValue)
                                simpleTrigger.RepeatInterval = TimeSpan.FromSeconds(trigger.RepeatInterval.Value);
                            simpleTrigger.JobName = jobName;
                            simpleTrigger.JobGroup = groupName;
                            simpleTrigger.MisfireInstruction = MisfireInstruction.SimpleTrigger.RescheduleNextWithExistingCount;

                            newTrigger = simpleTrigger;
                            break;

                        case "Cron":
                            CronTrigger cronTrigger = new CronTrigger(Guid.NewGuid().ToString(), groupName);
                            
                            var year = trigger.Year ?? "*";
                            var month = trigger.Month ?? "*";                             
                            var day = (string.IsNullOrEmpty(trigger.DayOfWeek) ? trigger.Day ?? "*" : "?");
                            var hour = trigger.Hour ?? "0";
                            var minute = trigger.Minute ?? "0";
                            var second = trigger.Second ?? "0";
                            var dayOfWeek = (day == "*") ? "?" : trigger.DayOfWeek ?? "?";

                            cronTrigger.CronExpressionString = string.Format("{0} {1} {2} {3} {4} {5} {6}", second, minute, hour, day, month, dayOfWeek, year);
                            cronTrigger.JobName = jobName;
                            cronTrigger.JobGroup = groupName;
                            cronTrigger.MisfireInstruction = MisfireInstruction.CronTrigger.DoNothing;

                            newTrigger = cronTrigger;
                            break;
                    }

                    if (newTrigger != null)
                    {                        
                        var quartzTrigger = quartzTriggers.FirstOrDefault(t => t.Name == trigger.Name && t.Group == groupName);
                        if (quartzTrigger != null)
                        {
                            if (TriggerIsDifferent(quartzTrigger, newTrigger))
                            {
                                scheduler.RescheduleJob(quartzTrigger.Name, quartzTrigger.Group, newTrigger);
                                log.Info(string.Format("Job {0}.{1} rescheduled trigger {2}", groupName, job.Name, newTrigger.Name));
                            }
                        }
                        else
                        {
                            scheduler.ScheduleJob(newTrigger);
                            log.Info(string.Format("Job {0}.{1} scheduled trigger {2}", groupName, job.Name, newTrigger.Name));
                        }
                    }
                }

                foreach(var quartzTrigger in quartzTriggers)
                {
                    if (!job.Triggers.Any(t => t.Name == quartzTrigger.Name && groupName == quartzTrigger.Group))
                    {
                        scheduler.UnscheduleJob(quartzTrigger.Name, quartzTrigger.Group);
                        log.Info(string.Format("Job {0}.{1} removed trigger {2}", groupName, job.Name, quartzTrigger.Name));
                    }
                }
            }
            else
            {                
                scheduler.PauseJob(quartzJob.Name, quartzJob.Group);
                log.Info(string.Format("Job {0}.{1} was disabled", groupName, job.Name));
            }            
        }

        private bool TriggerIsDifferent(Trigger ExistingTrigger, Trigger newTrigger)
        {
            if (!ExistingTrigger.GetType().Equals(newTrigger.GetType()))
            {
                return true;
            }

            //if (ExistingTrigger.GetType().Equals(typeof(SimpleTrigger)))
            //{
            //    SimpleTrigger trigger1 = (SimpleTrigger) ExistingTrigger;
            //    SimpleTrigger trigger2 = (SimpleTrigger) newTrigger;

            //    if (trigger1.Description != trigger2.Description)
            //    {
            //        return true;
            //    }

            //    if (trigger1.EndTimeUtc != trigger2.EndTimeUtc)
            //    {
            //        return true;
            //    }

            //    if (trigger1.)
            //}

            return false;
        }

        public void Shutdown()
        {
            foreach(var fileWatcher in directoryWatches.Values)
            {
                fileWatcher.Dispose();
            }
        }

        public void JobToBeExecuted(JobExecutionContext context)
        {
            
        }

        public void JobExecutionVetoed(JobExecutionContext context)
        {
            
        }

        public void JobWasExecuted(JobExecutionContext context, JobExecutionException jobException)
        {
            var jobFullName = string.Format("{0}.{1}", context.JobDetail.Group, context.JobDetail.Name);
            if (jobsToRemove.Contains(jobFullName))
            {
                if (scheduler.DeleteJob(context.JobDetail.Name, context.JobDetail.Group))
                {
                    lock (jobsToRemove)
                    {
                        jobsToRemove.Remove(jobFullName);
                    }
                }
            }
        }

        public string Name
        {
            get { return "XmlJobPlugin"; }
        }

        public void JobScheduled(Trigger trigger)
        {
            
        }

        public void JobUnscheduled(string triggerName, string triggerGroup)
        {
            
        }

        public void TriggerFinalized(Trigger trigger)
        {
            
        }

        public void TriggersPaused(string triggerName, string triggerGroup)
        {
            
        }

        public void TriggersResumed(string triggerName, string triggerGroup)
        {
            
        }

        public void JobAdded(JobDetail jobDetail)
        {
            var file = new FileInfo(Path.Combine(Path.Combine(@"Jobs\", jobDetail.Group), jobDetail.Name) + ".xml");

            var fileInfo = jobsAdded.Where(f => f.FullName.Equals(file.FullName, StringComparison.CurrentCultureIgnoreCase)).FirstOrDefault();
            if (fileInfo != null)
            {
                jobsAdded.Remove(fileInfo);
                return;
            }

            Core.Domain.Job job = new Core.Domain.Job();
            job.Description = jobDetail.Description;
            job.Enabled = Convert.ToBoolean(jobDetail.JobDataMap["Enabled"]);
            job.Tasks = jobDetail.JobDataMap["Tasks"] as TaskCollection;
            job.Triggers = jobDetail.JobDataMap["Triggers"] as TriggerCollection;
            job.Type = jobDetail.JobDataMap["Type"] as string;
            job.Description = "";
            job.Name = "";

            jobsSaved.Add(file);
            using(var fs = new StreamWriter(file.FullName))
            {
                fs.Write(job.XmlSerialize());
                fs.Close();
            }
        }

        public void JobDeleted(string jobName, string groupName)
        {
            
        }

        public void JobsPaused(string jobName, string jobGroup)
        {
            
        }

        public void JobsResumed(string jobName, string jobGroup)
        {
            
        }

        public void SchedulerError(string msg, SchedulerException cause)
        {
            
        }

        public void SchedulerInStandbyMode()
        {
            
        }

        public void SchedulerStarted()
        {
            
        }

        public void SchedulerShutdown()
        {
            
        }

        public void SchedulerShuttingdown()
        {
            
        }
    }
}